/*
 * Course:    CS 590 Web Services
 * Semseter:  Spring 2009
 * Professor: Sedlemeyer
 * Project:   CHEAT (Code Help Educational Assignment Tool)
 * Due Date:  May 1, 2009
 * Team:      Sarcasm
 *            (Baker, Jason; Mitchell, Robin; Shie, Keith)
 */
package cheat.assignment;

import cheat.authentication.InsufficientAccessException;
import cheat.authentication.InvalidSessionException;
import cheat.authentication.SecurityManagerLocal;
import cheat.entities.AccessType;
import cheat.entities.ArchiveType;
import cheat.entities.AssignmentArchive;
import cheat.entities.AssignmentCitation;
import cheat.entities.AssignmentKeyword;
import cheat.entities.AssignmentKeywordType;
import cheat.entities.AssignmentCustomProperty;
import cheat.entities.AssignmentExternalUrl;
import cheat.entities.AssignmentProperty;
import cheat.entities.AssignmentType;
import cheat.entities.Course;
import cheat.entities.IsoLanguage;
import cheat.entities.License;
import cheat.entities.Person;
import cheat.entities.ProgramLanguageSpec;
import cheat.entities.RegisteredUser;
import cheat.entities.UserSession;
import edu.ipfw.cs590.cheat.services.AssignmentWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceUnit;
import javax.persistence.Query;
import org.web_cat._1_0.assignment_sharing.Assignment;
import org.web_cat._1_0.assignment_sharing.Author;
import org.web_cat._1_0.assignment_sharing.Cite;
import org.web_cat._1_0.assignment_sharing.Command;
import org.web_cat._1_0.assignment_sharing.CustomProperties;
import org.web_cat._1_0.assignment_sharing.Estimated;
import org.web_cat._1_0.assignment_sharing.History;
import org.web_cat._1_0.assignment_sharing.Info;
import org.web_cat._1_0.assignment_sharing.Keywords;
import org.web_cat._1_0.assignment_sharing.Languages;
import org.web_cat._1_0.assignment_sharing.LearningObjectives;
import org.web_cat._1_0.assignment_sharing.PrerequisiteAssignments;
import org.web_cat._1_0.assignment_sharing.ProgrammingLanguage;
import org.web_cat._1_0.assignment_sharing.Property;
import org.web_cat._1_0.assignment_sharing.Repository;
import org.web_cat._1_0.assignment_sharing.Url;

/**
 * The assignment manager contains the functionality related to
 * assignment management.
 *
 * @author Keith Shie
 */
@Stateless
public class AssignmentManagerBean implements AssignmentManagerRemote, AssignmentManagerLocal {
  @PersistenceUnit(unitName = "CheatApp-ejbPU")
  private EntityManagerFactory emf;
  @EJB
  private SecurityManagerLocal securityManagerBean;
  @EJB
  private AssignmentManagerLocal assignmentManagerBean;

  private static Logger logger = Logger.getLogger(AssignmentManagerBean.class.getPackage().getName());

  private void appendContent( AssignmentProperty assignment, List<AssignmentKeyword> keywords, AssignmentKeywordType type, String tokenList ) {
    if (tokenList != null && tokenList.length() > 0) {
      final StringTokenizer st = new StringTokenizer( tokenList, "," );

      while (st.hasMoreTokens()) {
        final String token = st.nextToken().trim();
        if (token != null && token.length() > 0) {
          AssignmentKeyword content = new AssignmentKeyword();

          content.setContentValue( token );
          content.setUuid( assignment.getUuid() );
          content.setKeywordCode( type.getKeywordCode() );

          keywords.add( content );
        }
      }
    }
  }


  /**
   * Ensures that the specified Course is in persistent storage.
   *
   * <p>First attempts to locate an existing Course based upon the
   * label.</p>
   *
   * <p>If no such Course can be found, one is created.</p>
   *
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to generate the PK for the underlying License
   * Entity.  Providing this method as part of the public API is the most
   * straight forward way to gain this control.</p>
   *
   * @param label Course label
   * @param description Course description
   * @return Reference to Course entity within persistent storage.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public Course ensureCourse( String label, String description ) {
    logger.finer( "ensureCourse: start" );

    final EntityManager em = emf.createEntityManager();

    final Query qry = em.createNamedQuery( "Course.findByCourseLabel" );

    Course course = null;

    try {
      qry.setParameter( "label", label );
      course = (Course) qry.getSingleResult();
    } catch (NoResultException ex) {
      // No problem just add one
      course = new Course();

      course.setDescription( description );
      course.setLabel( label );
      em.persist( course );
    }
    logger.finer( "ensureCourse: end" );

    return course;
  }


  private void appendCourse( List<Course> courses, String tokenList ) {
    if (tokenList != null && tokenList.length() > 0) {
      final StringTokenizer st = new StringTokenizer( tokenList, "," );

      while (st.hasMoreTokens()) {
        final String token = st.nextToken().trim();
        if (token != null && token.length() > 0) {

          final int pos = token.indexOf( ':' );
          String label = null;
          String descrption = null;
          if (pos == -1) {
            label = token.trim();
          } else {
            label = token.substring( 0, pos ).trim();
            descrption = token.substring( pos + 1 ).trim();
          }

          courses.add( assignmentManagerBean.ensureCourse( label, descrption ) );
        }
      }
    }
  }


  private List<Person> buildAuthorList( Assignment meta, EntityManager em ) {
    /* Authors */
    final List<Author> authors = meta.getAuthor();
    final List<Person> managedAuthors = new ArrayList<Person>();
    for (Author a : authors) {
      managedAuthors.add( ensurePerson( em, a ) );
    }
    return managedAuthors;
  }


  private List<IsoLanguage> buildLanguageList( Assignment meta ) {
    /* Extract the list of languages from a list of comma separated languages */
    final List<Languages> meta_lang = meta.getLanguages();
    final List<IsoLanguage> isoLang = new ArrayList<IsoLanguage>();
    for (Languages lang : meta_lang) {
      final String langCdList = lang.getContent();
      final StringTokenizer st = new StringTokenizer( langCdList, "," );
      while (st.hasMoreTokens()) {
        final String langCd = st.nextToken();
        if (langCd != null && langCd.length() > 0) {
          isoLang.add( new IsoLanguage( langCd ) );
        }
      }
    }
    return isoLang;
  }


  private Person ensurePerson( EntityManager em, Author a ) {

    final Person p = new Person();

    p.setAffiliation( a.getAffiliation().getContent() );
    p.setEmail( a.getEmail().getContent() );
    p.setFirstName( a.getFirstName().getContent() );
    p.setLastName( a.getLastName().getContent() );
    p.setPhone( a.getPhone().getContent() );
    p.setUrl( a.getUrl().getContent() );

    Person existingPerson = null;

    try {
      existingPerson = (Person) em.createNamedQuery( "Person.findByEmail" ).
              setParameter( "email", a.getEmail().getContent() ).
              getSingleResult();

      p.setPersonId( existingPerson.getPersonId() );

      existingPerson = em.merge( p );
    } catch (NoResultException e) {
      // this is OK, it just means we need to also add the Person
      existingPerson = securityManagerBean.createNewPerson( p );
    }

    return existingPerson;
  }


  private AssignmentProperty createEntityGraph( EntityManager em, UserSession session, AssignmentInspector inspector ) {
    final DateFormat dtFmt = new SimpleDateFormat( "yyyy-MM-dd kk:mm:ss z" );

    AssignmentProperty ap = new AssignmentProperty();
    Assignment meta = inspector.getMetaBytes();

    ap.setAbstractTxt( meta.getAbstract().getContent() );
    ap.setCopyright( meta.getRights().getCopyright().getContent() );
    final License l = assignmentManagerBean.ensureLicense(
            meta.getRights().getUrl().getContent(),
            meta.getRights().getLicense().getContent() );
    ap.setLicenseId( l );
    ap.setTitle( meta.getTitle().
            getContent() );

    try {
      ap.setCreateDt( dtFmt.parse( meta.getDate().getCreated() ) );
      ap.setUpdDt( dtFmt.parse( meta.getDate().getModified() ) );
    } catch (ParseException ex) {
      Logger.getLogger( AssignmentManagerBean.class.getName() ).log( Level.SEVERE, null, ex );
    }

    final List<IsoLanguage> isoLang = buildLanguageList( meta );

    if (isoLang.size() > 0) {
      ap.setPrimaryLanguageCode( isoLang.get( 0 ) );
    } else {
      ap.setPrimaryLanguageCode( new IsoLanguage( "eng" ) );
    }
    ap.setIsoLangCollection( isoLang );

    ap.setOwnerId( session.getUser() );
    ap.setVersion( 1 );

    final Command cmd = meta.getCommand();
    if (cmd != null) {
      ap.setBuildCommand( cmd.getBuild().getContent() );
      ap.setTestCommand( cmd.getTest().getContent() );
    }

    if (meta.getType() != null) {
      ap.setAssignmentTypeCode( em.find( AssignmentType.class, meta.getType().getContent().toLowerCase() ) );
    }

    final LearningObjectives learnObj = meta.getLearningObjectives();
    if (learnObj != null) {
      ap.setLearningObjective( learnObj.getContent() );
    }

    final Estimated est = meta.getEstimated();
    if (est != null) {
      ap.setEstDifficulty( Short.valueOf( est.getDifficulty() ) );
      ap.setEstTime( est.getTime() );
      ap.setEstSize( Integer.valueOf( est.getSize() ) );
    }

    final Repository repos = meta.getRepository();
    if ( repos != null ){
      final History history = repos.getHistory();
      if ( history != null ){
        ap.setChangeDescription( history.getContent());
      }
    }

    ap = assignmentManagerBean.saveAssignment( ap );

    /* Extract the list of programming languages from a comma separated languages */
    final ProgrammingLanguage pl = meta.getProgrammingLanguage();

    if (pl != null) {
      final String langCd = pl.getName();

      ProgramLanguageSpec p = new ProgramLanguageSpec( ap.getUuid(), langCd );

      if (pl.getVersion() != null) {
        p.setLangVersion( pl.getVendor() );
      }
      if (pl.getVendor() != null) {
        p.setVendor( pl.getVendor() );
      }
      ap.setProgLangSpec( p );
    }

    /* Assignment Archive */
    AssignmentArchive arch = new AssignmentArchive();
    arch.setUuid( ap.getUuid() );
    arch.setFacultyArchive( inspector.getFacultyBytes() );
    arch.setStudentArchive( inspector.getStudentBytes() );
    arch.setArchiveType( em.find( ArchiveType.class,ArchiveType.ZIP_ARCHIVE_TYPE) );

    ap.setAssignmentArchive( arch );

    ap.setPersonCollection( buildAuthorList( meta, em ) );

    /* Keywords */
    final Keywords keywords = meta.getKeywords();

    if (keywords != null) {
      final List<AssignmentKeyword> keywordList = new ArrayList<AssignmentKeyword>();
      final List<Course> courses = new ArrayList<Course>();

      ap.setKeywordCollection( keywordList );
      ap.setCourseCollection( courses );

      appendContent( ap, keywordList, em.find( AssignmentKeywordType.class, AssignmentKeywordType.TOPIC_CD ),
              keywords.getTopics().getContent() );
      appendContent( ap, keywordList, em.find( AssignmentKeywordType.class, AssignmentKeywordType.OTHER_CD ),
              keywords.getOther().getContent() );
      appendContent( ap, keywordList, em.find( AssignmentKeywordType.class, AssignmentKeywordType.PREREQUISITE_CD ),
              keywords.getPrerequisites().getContent() );

      appendCourse( courses, keywords.getCourse().getContent() );
    }

    final PrerequisiteAssignments prereqs = meta.getPrerequisiteAssignments();
    if ( prereqs != null ){
      final List<AssignmentProperty> list = new ArrayList<AssignmentProperty>();
      ap.setPrerequisiteCollection(list);

      final StringTokenizer st = new StringTokenizer( prereqs.getContent());

      while ( st.hasMoreTokens() ){
        final Integer prereqUuid = Integer.valueOf( st.nextToken() );

        AssignmentProperty prereqAssignment = em.find( AssignmentProperty.class, prereqUuid);
        if ( prereqAssignment != null ){
          list.add( prereqAssignment);
        }
      }
    }

    /* Info */
    final Info info = meta.getInfo();
    if (info != null) {
      final List<AssignmentExternalUrl> urls = new ArrayList<AssignmentExternalUrl>();
      ap.setAssignmentExternalUrlCollection( urls );

      for (Url u : info.getUrl()) {
        urls.add( new AssignmentExternalUrl( ap.getUuid(), u.getContent() ) );
      }

      final List<AssignmentCitation> cites = new ArrayList<AssignmentCitation>();
      ap.setAssignmentCitationCollection( cites );

      for (Cite c : info.getCite()) {
        cites.add( new AssignmentCitation( ap.getUuid(), c.getContent() ) );
      }
    }

    /* Custom Properties */
    final CustomProperties custProps = meta.getCustomProperties();
    if (custProps != null) {
      final List<AssignmentCustomProperty> props = new ArrayList<AssignmentCustomProperty>();
      ap.setAsgnCustPropCollection( props );

      for (Property p : custProps.getProperty()) {
        props.add( new AssignmentCustomProperty( ap.getUuid(), p.getName(), p.getValue() ) );
      }
    }

    // TODO: Complete mappings from JAXB binding doc to DB entities
    ap = em.merge( ap );

    return ap;
  }


  private AssignmentProperty submitAssignmentWorker( EntityManager em, UserSession session, byte[] assignmentZip )
          throws IOException, InvalidAssignmentArchiveException {

    AssignmentProperty ap = null;
    final AssignmentInspector inspector = AssignmentInspector.build( assignmentZip );

    if (inspector == null) {
      throw new InvalidAssignmentArchiveException( "Unknown problem with assignment submission.");
    } else {
      ap = createEntityGraph( em, session, inspector );
    }

    return ap;
  }


  private void verifyFacultyAccess( UserSession session ) throws InsufficientAccessException {
    if (!session.getUser().getAccessCode().equals( AccessType.FACULTY_TYPE )) {
      throw new InsufficientAccessException( "Only a faculty member can perform this operation." );
    }
  }


  /**
   * Submit an assignment to the assignment sharing system.
   *
   * <p>Only FACULTY are permitted to submit assignments.</p>
   *
   * <p>Asignments archives are separated into three parts; the metadata,
   * the student files, and the faculty files.</p>
   *
   * <p>Metadata is stored into aa collection of entities to facilitate
   * searching.</p>
   *
   * <p>Student only files are placed into an student archive entity. and
   * likewise for the faculty files.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentZip bytes of zip file containing the assignment archive.
   *
   * @return uuid of the newly added assignment.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRED)
  public int submitAssignment( int sessionId, byte[] assignmentZip ) throws InvalidSessionException,
                                                                            InsufficientAccessException,
                                                                            InvalidAssignmentArchiveException {
    logger.finer( "submitAssignment: start" );
    final UserSession session = securityManagerBean.getSession( sessionId );
    int uuid = 0;

    verifyFacultyAccess( session );

    try {
      final EntityManager em = emf.createEntityManager();

      final AssignmentProperty ap = submitAssignmentWorker( em, session, assignmentZip );

      if (ap == null) {
        throw new InvalidAssignmentArchiveException( "Assignment submission failed for an unknown reason.");
      } else {
        uuid = ap.getUuid();
      }
    } catch (IOException ex) {
      Logger.getLogger( AssignmentManagerBean.class.getName() ).
              log( Level.SEVERE, ex.getMessage(), ex );
    }

    logger.finer( "submitAssignment: end" );

    return uuid;
  }


  /**
   * Remove the specified assignment from the system.
   *
   * <p>Assignments are logically removed from the system so as to
   * preserve referential integrity.  An assignment logically removed
   * will no longer be returned via queries through the system.</p>
   *
   * <p>Only ADMIN and FACULTY can remove an assignment.</p>
   *
   * <p>ADMIN can remove any assignment.</p>
   *
   * <p>FACULTY can only remove an assignment which they are either an
   * owner or an author.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentUuid Unique id of the assignment for which metadata is
   * required.
   *
   * @return true if the assignment has been removed.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public boolean removeAssignment( int sessionId, int assignmentUuid ) throws InvalidSessionException,
                                                                              InsufficientAccessException,
                                                                              AssignmentNotFoundException {
    logger.finer( "removeAssignment: start" );
    final UserSession session = securityManagerBean.getSession( sessionId );
    final RegisteredUser user = session.getUser();
    final boolean student = user.getAccessCode().equals( AccessType.STUDENT_TYPE );

    boolean result = false;

    if (student) {
      throw new InsufficientAccessException( "Student unable to remove assignments" );
    } else {
      final EntityManager em = emf.createEntityManager();
      final AssignmentProperty ap = em.find( AssignmentProperty.class, assignmentUuid );

      if ( ap == null ){
        throw new AssignmentNotFoundException( "uuid: " + assignmentUuid );
      } else {
        final boolean faculty = user.getAccessCode().equals( AccessType.FACULTY_TYPE );
        boolean removalAllowed = false;

        if (!faculty) {
          removalAllowed = true;
        } else if (ap.getOwnerId().equals( user )) {
          removalAllowed = true;
        } else {
          for (Person author : ap.getPersonCollection()) {
            if (author.equals( user.getPerson() )) {
              removalAllowed = true;
              break;
            }
          }
        }

        if (removalAllowed) {
          ap.setDeleted( true );

          result = true;
        } else {
          throw new InsufficientAccessException( "Faculty must be an author to assignment" );
        }
      }
    }

    logger.finer( "removeAssignment: end" );

    return result;
  }


  /**
   * Replace the specified assignment in the system.
   * 
   * <p>The previous assignment is logically removed from the system 
   * so as to preserve referential integrity.  An assignment logically
   * removed will no longer be returned via queries through the system.</p>
   * 
   * <p>Only ADMIN and FACULTY can replace an assignment.</p>
   * 
   * <p>ADMIN can replace any assignment.</p>
   * 
   * <p>FACULTY can only replace an assignment which they are either an
   * owner or an author.</p>
   * 
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param priorAssignmentUuid Unique id of the assignment for which metadata is
   * required.
   * 
   * @param assignmentZip bytes of zip file containing the assignment archive.
   *
   * @return the new uuid.
   * 
   * @throws cheat.authentication.InvalidSessionException
   * @throws InsufficientAccessException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public int replaceAssignment( int sessionId, int priorAssignmentUuid, byte[] assignmentZip ) throws
          InvalidSessionException, InsufficientAccessException, AssignmentNotFoundException, InvalidAssignmentArchiveException {

    logger.finer( "replaceAssignment: start" );
    final EntityManager em = emf.createEntityManager();

    final UserSession session = securityManagerBean.getSession( sessionId );
    int newAssignmentUuid = 0;

    if (!session.getUser().getAccessCode().equals( AccessType.FACULTY_TYPE )) {
      throw new InsufficientAccessException( "Only FACULTY may replace an assignment" );
    } else {
      final AssignmentProperty prior = em.find( AssignmentProperty.class, priorAssignmentUuid );

      if (prior == null) {
        throw new AssignmentNotFoundException( "assignment not found: uuid = " + priorAssignmentUuid );
      } else {
        AssignmentProperty newAssignment = null;
        try {
          newAssignment = submitAssignmentWorker( em, session, assignmentZip );

          if (newAssignment == null) {
            throw new InvalidAssignmentArchiveException( "Assignment replace failed for an unknown reason.");
          } else {
            newAssignmentUuid = newAssignment.getUuid();
            AssignmentProperty root = prior.getRoot();

            newAssignment.setRoot( (root == null) ? prior : root );
            newAssignment.setParent( prior );
            newAssignment.setVersion( prior.getVersion() + 1 );
          }
        } catch (IOException ex) {
          Logger.getLogger( AssignmentManagerBean.class.getName() ).log( Level.SEVERE, ex.getMessage(), ex );
          throw new InvalidAssignmentArchiveException( "Assignment replace failed for an unknown reason.");
        }
      }
    }

    logger.finer( "replaceAssignment: end" );

    return newAssignmentUuid;
  }


  /**
   * Merge the specified archive with an existing assignment within the assignment sharing system.
   *
   * <p>Only an assignment owner can perform this operation.</p>
   *
   * <p>files within the mergeZip archive overwrite files within the existing archive with the
   * same name.  New files within the mergeZip archive are added the the existing assignment.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentUuid Unique id of the existing assignment.
   *
   * @param mergeZip bytes of zip file containing the archive to be merged.
   *
   * @see #submitAssignment
   *
   * @throws cheat.assignment.AssignmentNotFoundException
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   * @throws cheat.assignment.InvalidAssignmentArchiveException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void mergeAssignment( int sessionId, int assignmentUuid, byte[] mergeZip ) throws AssignmentNotFoundException,
                                                                                           InvalidSessionException,
                                                                                           InsufficientAccessException,
                                                                                           InvalidAssignmentArchiveException  {
  }


  /**
   * Get the list of files contained within this assignment.
   *
   * <p>Requests from users with FACULTY access recieve all content.
   * STUDENT's do not receive content specifically designated for
   * FACULTY.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentUuid Unique id of the assignment for which the files list is
   * desired.
   *
   * @return a list of files contained within this assignment.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.assignment.AssignmentNotFoundException
   * @throws cheat.authentication.InsufficientAccessException
   */
  public List<String> getAssignmentFileList( int sessionId, int assignmentUuid ) throws InvalidSessionException,
                                                                                        AssignmentNotFoundException,
                                                                                        InsufficientAccessException {
    final UserSession session = securityManagerBean.getSession( sessionId );
    final EntityManager em = emf.createEntityManager();
    final List<String> files = new ArrayList<String>();

    final AssignmentProperty ap = em.find( AssignmentProperty.class, assignmentUuid );

    if (ap == null || ap.isDeleted()) {
      throw new AssignmentNotFoundException( "Assignment " + assignmentUuid +
              " does not exist" );
    } else {
      try {
        final boolean student = session.getUser().getAccessCode().equals( AccessType.STUDENT_TYPE );

        final List<byte[]> sections = new ArrayList<byte[]>();
        sections.add( ap.getAssignmentArchive().getStudentArchive() );

        if (!student) {
          sections.add( ap.getAssignmentArchive().getFacultyArchive() );
        }

        files.add( AssignmentInspector.ASSIGNMENT_META_FILE );
        ZipEntry ze;

        for (byte[] b : sections) {
          final ByteArrayInputStream bais = new ByteArrayInputStream( b );
          final ZipInputStream zis = new ZipInputStream( bais );

          while (null != (ze = zis.getNextEntry())) {
            files.add( ze.getName() );

//            final byte[] buffer = new byte[10240];
//            while (-1 != (bytesRead = zis.read( buffer ))) {
//            }

            zis.closeEntry();
          }
          zis.close();
        }
      } catch (IOException ex) {
        Logger.getLogger( AssignmentManagerBean.class.getName() ).log( Level.SEVERE, ex.getMessage(), ex );
      }
      Collections.sort( files );
    }

    return files;
  }


  /**
   * Remove files from the assignment.
   *
   * <p>Only an assignment owner can perform this operation.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentUuid Unique id of the assignment for which the files list is
   * desired.
   *
   * @param removalList list of files to be removed.
   *
   * @throws cheat.assignment.AssignmentNotFoundException
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.authentication.InsufficientAccessException
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void removeAssignmentFile( int sessionId, int assignmentUuid, List<String> removalList ) throws
          AssignmentNotFoundException,
          InvalidSessionException,
          InsufficientAccessException {
  }


  /**
   * Ensures that the specified License is in persistent storage.
   *
   * <p>First attempts to locate an existing License based upon the
   * url.</p>
   *
   * <p>If no such License can be found, one is created.</p>
   *
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to generate the PK for the underlying License
   * Entity.  Providing this method as part of the public API is the most
   * straight forward way to gain this control.</p>
   *
   * @param url License url
   * @param description License description
   * @return Reference to License entity within persistent storage.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public License ensureLicense( String url, String description ) {
    logger.finer( "ensureLicense: start" );

    final EntityManager em = emf.createEntityManager();

    final Query qry = em.createNamedQuery( "License.findByUrl" );

    License l = null;

    try {
      qry.setParameter( "url", url );
      l = (License) qry.getSingleResult();
    } catch (NoResultException ex) {
      // No problem just add one
      l = new License();

      l.setDescr( description );
      l.setUrl( url );
      em.persist( l );
    }
    logger.finer( "ensureLicense: end" );

    return l;
  }


  /**
   * Saves out the core part on an Assignment.
   *
   * <p>Ideally, this method would not be part of a public API.  However, we
   * need to gain control over the transaction so that we can force the
   * persistance layer to generate the PK for the underlying AssignmentProperty
   * Entity.  Providing this method as part of the public API is the most
   * straight forward way to gain this control.</p>
   *
   * @param assignment core part of the assignment to be saved.
   * The object passed in should not contain references to other entities
   * which have their our requirement to gerenate a PK.
   *
   * @return The AssignmentProperty which has been saved.
   */
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public AssignmentProperty saveAssignment( AssignmentProperty assignment ) {
    logger.finer( "saveAssignment: start" );

    final EntityManager em = emf.createEntityManager();

    em.persist( assignment );

    logger.finer( "saveAssignment: end" );

    return assignment;
  }


  @SuppressWarnings("unchecked")
  private AssignmentWrapper translateAssignmentMeta( AssignmentProperty ap ) {
    logger.finer( "translateAssignmentMeta: start" );
    AssignmentWrapper wrapper = null;

    if (ap != null && !ap.isDeleted()) {
      final byte[] meta = new AssignmentBuilder( ap ).build();

      wrapper = new AssignmentWrapper( meta );
    }

    logger.finer( "translateAssignmentMeta: end" );
    return wrapper;
  }


  /**
   * Obtain the metadata associated with the specified assignmentUuid.
   *
   * @param assignmentUuid Unique id of the assignment for which metadata is
   * required.
   *
   * @return An assignment's metadata object wrapped so as to allow it to
   * be returned to the caller via Java Serialization.  The contents of the
   * wrapped object are a byte[] containing the bytes of the metadata file.
   *
   * @throws cheat.authentication.InvalidSessionException
   */
  @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
  public AssignmentWrapper getAssignmentMeta( int assignmentUuid ) throws InvalidSessionException, AssignmentNotFoundException {
    logger.finer( "getAssignmentMeta(uuid): start" );
    final EntityManager em = emf.createEntityManager();

    final AssignmentProperty ap = em.find( AssignmentProperty.class, assignmentUuid );

    if ( ap == null ){
      throw new AssignmentNotFoundException( "uuid: " + assignmentUuid );
    }
    logger.finer( "getAssignmentMeta: end" );

    return translateAssignmentMeta( ap );
  }


  /**
   * Extract the assignment specified by <code>assignmentUuid</code>.
   *
   * <p>Requests from users with FACULTY access recieve all content.
   * STUDENT's do not receive content specifically designated for
   * FACULTY.</p>
   *
   * @param sessionId Id of UserSession to use to determine access level.
   *
   * @param assignmentUuid Unique id of the assignment for which metadata is
   * required.
   *
   * @return bytes of zip file containing the assignment archive.
   *
   * @throws cheat.authentication.InvalidSessionException
   * @throws cheat.assignment.AssignmentNotFoundException
   */
  public byte[] getAssignment( int sessionId, int assignmentUuid ) throws InvalidSessionException,
                                                                          AssignmentNotFoundException {
    logger.finer( "getAssignment: start" );
    final UserSession session = securityManagerBean.getSession( sessionId );
    final EntityManager em = emf.createEntityManager();
    byte[] result = null;

    final AssignmentProperty ap = em.find( AssignmentProperty.class, assignmentUuid );

    if (ap == null || ap.isDeleted()) {
      throw new AssignmentNotFoundException( "Assignment " + assignmentUuid +
              " does not exist" );
    } else {
      try {
        final boolean student = session.getUser().getAccessCode().equals( AccessType.STUDENT_TYPE );

        final List<byte[]> sections = new ArrayList<byte[]>();
        sections.add( ap.getAssignmentArchive().getStudentArchive() );

        if (!student) {
          sections.add( ap.getAssignmentArchive().getFacultyArchive() );
        }

        byte[] meta = new AssignmentBuilder( ap ).build();
        int expectedSize = meta.length;
        for (byte[] b : sections) {
          expectedSize += b.length;
        }

        final ByteArrayOutputStream baos = new ByteArrayOutputStream( expectedSize );
        final ZipOutputStream zos = new ZipOutputStream( baos );

        ZipEntry ze = new ZipEntry( AssignmentInspector.ASSIGNMENT_META_FILE );
        ze.setSize( meta.length );
        ze.setTime( ap.getUpdDt().getTime() );

        zos.putNextEntry( ze );
        zos.write( meta );
        zos.closeEntry();

        for (byte[] b : sections) {
          final ByteArrayInputStream bais = new ByteArrayInputStream( b );
          final ZipInputStream zis = new ZipInputStream( bais );

          while (null != (ze = zis.getNextEntry())) {
            zos.putNextEntry( ze );

            final byte[] buffer = new byte[10240];
            int bytesRead;
            while (-1 != (bytesRead = zis.read( buffer ))) {
              zos.write( buffer, 0, bytesRead );
            }

            zos.closeEntry();
            zis.closeEntry();
          }
          zis.close();
        }
        zos.close();

        result = baos.toByteArray();
        logger.fine( "getAssignment: result size = " + result.length );
      } catch (IOException ex) {
        Logger.getLogger( AssignmentManagerBean.class.getName() ).log( Level.SEVERE, null, ex );
      }
    }

    logger.finer( "getAssignment: end" );

    return result;
  }


  private List<AssignmentWrapper> wrapAssignments( EntityManager em,
                                                   final List<AssignmentProperty> assignments ) {
    final List<AssignmentWrapper> aJAXB = new ArrayList<AssignmentWrapper>();

    for (AssignmentProperty ap : assignments) {
//      em.refresh(ap);

      aJAXB.add( translateAssignmentMeta( ap ) );
    }
    return aJAXB;
  }


  /**
   * Search for assignments which have the specified programming
   * language.
   * 
   * @param language the program language to search for.
   * 
   * @return list of wrapped assignments matching the criteria.
   */
  @SuppressWarnings("unchecked")
  @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
  public List<AssignmentWrapper> findByLanguage( String language ) {
    final EntityManager em = emf.createEntityManager();


    final List<AssignmentProperty> assignments = em.createQuery(
            "select distinct a from AssignmentProperty a where lower(a.progLangSpec.name) = lower(:name) and a.deleted = false" ).
            setParameter( "name", language ).
            getResultList();

    return wrapAssignments( em, assignments );
  }


  /**
   * Search for assignments which have the specified keywords.
   *
   * @param keyword the list of keywords to search for.
   *
   * @return list of wrapped assignments matching the criteria.
   */
  @SuppressWarnings("unchecked")
  public List<AssignmentWrapper> findByKeyword( List<String> keyword ) {
    final EntityManager em = emf.createEntityManager();
    logger.finer( "findByKeyword: start" );

    final StringBuilder qryStmt = new StringBuilder();
    List<AssignmentProperty> assignments = new ArrayList<AssignmentProperty>();

    try {
    if ( keyword.size() > 0 ){
    for (String k : keyword) {
      if (qryStmt.length() > 0) {
        qryStmt.append( ", " );
      }
      qryStmt.append( '\'' );
      qryStmt.append( k.toLowerCase() );
      qryStmt.append( '\'' );
    }

    qryStmt.insert( 0,
            "select distinct a from AssignmentProperty a, in ( a.keywordCollection ) content where lower(content.contentValue) in ( " );
    qryStmt.append( ") and a.deleted = false" );

    assignments = em.createQuery( qryStmt.toString() ).
            getResultList();
    }
    } catch ( Exception ex ){
        logger.log( Level.SEVERE, ex.getMessage(), ex );
    } finally {
      logger.finer( "findByKeyword: end" );
    }

    return wrapAssignments( em, assignments );
  }


  /**
   * Search for assignments which have the specified course.
   *
   * @param course the course to search for.
   *
   * @return list of wrapped assignments matching the criteria.
   */
  @SuppressWarnings("unchecked")
  public List<AssignmentWrapper> findByCourse( String course ) {
    final EntityManager em = emf.createEntityManager();

    final List<AssignmentProperty> assignments = em.createQuery(
            //            "select distinct a from AssignmentProperty a where ( a.personCollection.email = :email or a.ownerId.person.email = :email) and a.deleted = false" ).
            "select distinct a from AssignmentProperty a, in ( a.courseCollection ) course where lower(course.label) = lower(:label) and a.deleted = false" ).
            setParameter( "label", course ).
            getResultList();

    return wrapAssignments( em, assignments );
  }


  /**
   * Search for assignments which have the specified email.
   *
   * @param email the author's email to search for.
   *
   * @return list of wrapped assignments matching the criteria.
   */
  @SuppressWarnings("unchecked")
  public List<AssignmentWrapper> findByAuthor( String email ) {
    final EntityManager em = emf.createEntityManager();

    final List<AssignmentProperty> assignments = em.createQuery(
            //            "select distinct a from AssignmentProperty a where ( a.personCollection.email = :email or a.ownerId.person.email = :email) and a.deleted = false" ).
            "select distinct a from AssignmentProperty a, in ( a.personCollection ) author where ( author.email = :email or a.ownerId.person.email = :email) and a.deleted = false" ).
            setParameter( "email", email ).
            getResultList();

    return wrapAssignments( em, assignments );
  }


  /**
   * Get the history of an assignment.
   *
   * @param assignmentUuid Unique id of the assignment for which history is
   * desired.
   *
   * @return a list of the ancestors of the specified assignment.<p><i>Does include the specified assignment.</i>  Will
   * return a list ith no elements if there is no ansectry.</p>
   * @throws cheat.assignment.AssignmentNotFoundException
   */
  public List<AssignmentWrapper> getAssignmentHistory( int assignmentUuid ) throws AssignmentNotFoundException {
    final EntityManager em = emf.createEntityManager();

    final List<AssignmentProperty> path = new ArrayList<AssignmentProperty>();
    AssignmentProperty current = em.find( AssignmentProperty.class, assignmentUuid );

    if (current == null) {
      throw new AssignmentNotFoundException( "uuid: " + assignmentUuid );
    }

    current = current.getParent();

    while (current != null) {
      if (!current.isDeleted()) {
        path.add( current );
      }

      current = current.getParent();
    }

    return wrapAssignments( em, path );
  }


  /**
   * Get the owner of an assignment.
   *
   * @param assignmentUuid Unique id of the assignment for which the files list is
   * desired.
   *
   * @return email address for the assignment's owner.
   *
   * @throws cheat.assignment.AssignmentNotFoundException
   */
  public String getOwner( int assignmentUuid ) throws AssignmentNotFoundException {
    final EntityManager em = emf.createEntityManager();

    AssignmentProperty current = em.find( AssignmentProperty.class, assignmentUuid );

    if (current == null) {
      throw new AssignmentNotFoundException( "uuid: " + assignmentUuid );
    }

    return current.getOwnerId().getPerson().getEmail();
  }
}
